<?php
/**
 * Manejador principal de conexiones y consultas
 * @author Sahib Alejandro Jaramillo Leo
 */
class SahORMHandler
{
  /**
   * Especifica a _makeSaveData() hacer placeholders de tipo INSERT
   * @var int
   */
  const PLACEHOLDERS_TYPE_INSERT = 1;
  
  /**
   * Especifica a _makeSaveData() hacer placeholders de tipo UPDATE
   * @var int
   */
  const PLACEHOLDERS_TYPE_UPDATE = 2;
  
  /**
   * Array de objetos PDO, uno por cada base de datos
   * @var array
   */
  private static $_pdo_objects = array();
  
  /**
   * Informacion sobre las columnas de las tablas
   * @var array
   */
  private static $_columns_info = array();
  
  /**
   * Devuelve el objeto PDO relacionado con la conexión de nombra $conn_name
   * @return PDO
   */
  public static function getPDO($conn_name)
  {
    /*
     * Checamos si existe el objeto PDO relacionado con $conn_name, si no
     * existe lo creamos. Al final devolvemos el objeto PDO solicitado.
     */
    if(!isset(self::$_pdo_objects[$conn_name])){
      
      // Cargar configuración
      $database = array();
      require dirname(__FILE__) . '/config.php';
      
      // Configurar options
      $database[$conn_name]['options'][PDO::ATTR_ERRMODE]
        = PDO::ERRMODE_EXCEPTION;
      
      // Crear objeto PDO para $conn_name
      self::$_pdo_objects[$conn_name] = new PDO(
        "mysql:host={$database[$conn_name]['host']};"
        ."dbname={$database[$conn_name]['database']}",
        $database[$conn_name]['user'],
        $database[$conn_name]['password'],
        $database[$conn_name]['options']
      );
      
      // SET NAMES (No es recomendable usar MYSQL_ATTR_INIT_COMMAND)
      self::$_pdo_objects[$conn_name]->query(
        "SET NAMES '{$database[$conn_name]['charset']}';");
    }
    return self::$_pdo_objects[$conn_name]; 
  }
  
  /**
   * Inserta los campos $fields en un nuevo registro en la tabla $table
   * utilizando la conexion $conn_name
   * @param array $fields Valores de insercion
   * @param string $table Nombre de la tabla
   * @param string $conn_name Nombre de la conexion
   * @return PDOStatement Resultado del insert
   */
  public static function insert($table, $fields, $conn_name = 'default')
  {
    // Crear string de campos para actualizar
    $field_keys = implode(',', self::_quoteFields(array_keys($fields)));
    
    // Generar placeholders y arguments a partir de $fields
    self::_makeSaveData($fields, $placeholders, $arguments,
      self::PLACEHOLDERS_TYPE_INSERT);
    $placeholders = implode(',', $placeholders);
    
    return self::query(
      "INSERT INTO `$table`($field_keys)VALUES($placeholders)",
      $arguments,
      $conn_name
    );
  }
  
  /**
   * Ejecuta un SELECT $fields sobre la tabla $table con los
   * criterios $criteria utilizando la conexion $conn_name
   * @param string $fields Nombre de los campos separados por coma,
   *                       asterisco para seleccionar todos.
   * @param string $table Nombre de la tabla
   * @param array $criteria Criteria de seleccion
   * @param string $conn_name Nombre de conexion, por default "default"
   * @return PDOStatement Resultado del select
   */
  public static function select($fields, $table, $criteria,
    $conn_name = 'default')
  {
    // Procesar criteria
    $criteria = self::_processCriteria($criteria);
    // Crear sentencia SQL
    return self::query(
      "SELECT $fields FROM `$table`" . $criteria['where'] .
      $criteria['order_by'] . $criteria['limit'],
      $criteria['arguments'],
      $conn_name
    );
  }
  
  /**
   * Genera un UPDATE de los campos $fields sobre la tabla $tabla con los
   * criterios $criteria en la conexion $conn_name
   * @param array $fields Matriz de campos
   * @param string $table Nombre de la tabla
   * @param array $criteria Criterios para el update
   * @param string $conn_name Nombre de la conexion
   * @return PDOStatement Resultado del update
   */
  public static function update($table, $fields, $criteria = array(),
    $conn_name = 'default')
  {
    // Generar placeholders y arguments a partir de $fields
    self::_makeSaveData($fields, $placeholders, $arguments,
      self::PLACEHOLDERS_TYPE_UPDATE);
    $placeholders = implode(',', $placeholders);
    
    $criteria = self::_processCriteria($criteria);
    $criteria['arguments'] = array_merge($criteria['arguments'], $arguments);
    
    return self::query("UPDATE `$table` SET $placeholders" .
      $criteria['where'] . $criteria['limit'],
      $criteria['arguments'],
      $conn_name);
  }
  
  /**
   * Ejecuta un delete sobre $table con los criterios $criteria en la
   * conexion $conn_name
   * @param array $criteria Criterios para el DELETE
   * @param string $table Nombre de tabla
   * @param string $conn_name Nombre de conexion
   * @return PDOStatement
   */
  public static function delete($table, $criteria, $conn_name = 'default')
  {
    $criteria = self::_processCriteria($criteria);
    return self::query(
      "DELETE FROM `$table`" . $criteria['where'] . $criteria['limit'],
      $criteria['arguments'],
      $conn_name);
  }
  
  /**
   * Procesa $criteria para utilizar sus valores directamente en una
   * sentencia SQL
   * @param array $criteria Array de entrada con valores de criteria
   * @return array Array de entrada preparado para usar en SQL
   */
  private static function _processCriteria($criteria)
  {
    // where
    if(!isset($criteria['where'])){
      $criteria['where'] = NULL;
    } else {
      $criteria['where'] = " WHERE {$criteria['where']} ";
    }
    
    // order by
    if(!isset($criteria['order_by'])){
      $criteria['order_by'] = NULL;
    } else {
      $criteria['order_by'] = " ORDER BY {$criteria['order_by']} ";
    }
    
    // limit
    if(!isset($criteria['limit'])){
      $criteria['limit'] = NULL;
    } else {
      if(!isset($criteria['skip'])){
        $criteria['limit'] = ' LIMIT ' . $criteria['limit'];
      } else {
        $criteria['limit'] = " LIMIT {$criteria['skip']},{$criteria['limit']} ";
      }
    }
    
    // arguments
    if(!isset($criteria['arguments'])){
      $criteria['arguments'] = array();
    }
    
    return $criteria;
  }
  
  /**
   * Escapa cada elemento del array $fields con backquotes y devuelve el
   * array con los elementos escapados.
   * @param array $fields
   * @return array
   */
  private static function _quoteFields($fields)
  {
    return array_map(create_function('$e', 'return "`$e`";'), $fields);
  }
  
  /**
   * Genera los datos a partir de $fields, para $placeholders y
   * $arguments necesarios para utilizar el las consultas como INSERT o UPDATE
   * @param array Matriz de valores
   * @param array Salida de array de placeholders
   * @param array Salida de matriz de arguments
   */
  private static function _makeSaveData($fields, &$placeholders, &$arguments,
    $pholder_type)
  {
    $arguments = array();
    foreach($fields as $key => $value)
    {
      if(!($value instanceof SahORMSqlExpression)){
        if($pholder_type == self::PLACEHOLDERS_TYPE_INSERT){
          $placeholders[] = ":$key";
        } elseif($pholder_type == self::PLACEHOLDERS_TYPE_UPDATE) {
          $placeholders[] = "$key=:$key";
        }
        $arguments[":$key"] = $value;
      } else {
        if($pholder_type == self::PLACEHOLDERS_TYPE_INSERT){
          $placeholders[] = $value->getExpression();
        } elseif($pholder_type == self::PLACEHOLDERS_TYPE_UPDATE){
          $placeholders[] = "$key=" . $value->getExpression();
        }
        $arguments = array_merge($arguments, $value->getArguments());
      }
    }
  }
  
  /**
   * Ejecuta la consulta $sql con los argumentos $arguments con el
   * objeto PDO correspondiente a $conn_name
   * @param string $sql Consulta SQL
   * @param array $arguments Array de argumentos para los placeholders
   * @param string $conn_name Nombre de la conexion, por default "default"
   * @return PDOStatement Resultado de la consulta ejecutada
   */
  public static function query($sql, $arguments = NULL, $conn_name = 'default')
  {
    try{
      $St = self::getPDO($conn_name)->prepare($sql);
      $St->execute($arguments);
      return $St;
    }catch(PDOException $e){
      throw new SahORMException(
        'Ocurrio un error al consultar la base de datos',
        SahORMException::QUERY_ERROR, $e->getMessage(), $e->getCode());
      
    }
  }
  
  /**
   * Devuelve la información de las columas de la tabla $table sobre la
   * conexion $conn_name
   * @param string $table Nombre de la tabla
   * @param string $conn_name Nombre de conexion
   * @return array Lista de objetos con informacion sobre cada columna
   */
  public static function getColumnsInfo($table, $conn_name)
  {
    if(!isset(self::$_columns_info[$table])){
      self::$_columns_info[$table] = self::query("SHOW COLUMNS FROM `$table`",
        NULL, $conn_name)->fetchAll(PDO::FETCH_OBJ);
    }
    return self::$_columns_info[$table];
  }
  
  /**
   * Fusiona 2 criterias diferentes, combinando sus clausulas WHERE y arguments
   * devuelve el criteria combinado.
   * @param array $criteria_one
   * @param array $criteria_two
   * @return array
   */
  public static function mergeCriteria($criteria_one, $criteria_two)
  {
    $criteria = array();
    
    // Fusionar WHERE
    
    if(isset($criteria_one['where']) AND isset($criteria_two['where'])){
      // Existen los 2 WHERE
      $criteria['where'] = '(' . $criteria_one['where'] . ')AND('.
        $criteria_two['where'] . ')';
      unset($criteria_one['where'], $criteria_two['where']);
    } elseif(isset($criteria_one['where'])) {
      // Existe solo el WHERE del PRIMER criterio
      $criteria['where'] = $criteria_one['where'];
      unset($criteria_one['where']);
    } elseif(isset($criteria_two['where'])) {
      // Existe solo el WHERE del SEGUNDO criterio
      $criteria['where'] = $criteria_two['where'];
      unset($criteria_two['where']);
    }
    
    // Fusionar ARGUMENTS
    
    if(isset($criteria_one['arguments']) AND isset($criteria_two['arguments'])){
      $criteria['arguments'] = array_merge($criteria_one['arguments'],
        $criteria_two['arguments']);
      unset($criteria_one['arguments'], $criteria_two['arguments']);
    } elseif(isset($criteria_one['arguments'])) {
      $criteria['arguments'] = $criteria_one['arguments'];
      unset($criteria_one['arguments']);
    } elseif(isset($criteria_two['arguments'])) {
      $criteria['arguments'] = $criteria_two['arguments'];
      unset($criteria_two['arguments']);
    }
    
    return array_merge($criteria, $criteria_one, $criteria_two);
  }

  /**
   * Realiza un select count sobre la tabla $table con los criterios
   * $criteria utilizando la conexion $conn_name
   * @param array $criteria Criterios para el SELECT COUNT(*)
   * @param string $table Nombre de tabla
   * @param string $conn_name Nombre de conexion
   * @return int Conteo total
   */
  public static function count($criteria, $table, $conn_name = 'default')
  {
    return (int)self::select('COUNT(*)', $table, $criteria, $conn_name)
      ->fetchColumn(0);
  }
}
